/**
 *  Copyright 2010 Kyle O'Connor, Justin Arling, Mike Mercieca
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  any later version.
 *  
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */
package edu.drexel.sd0910.ece01.aqmon.manager;

import java.security.Security;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

import org.apache.log4j.Logger;

import edu.drexel.sd0910.ece01.aqmon.config.ConfigKeys;
import edu.drexel.sd0910.ece01.aqmon.data.ActiveNode;
import edu.drexel.sd0910.ece01.aqmon.gui.MainModel;
import edu.drexel.sd0910.ece01.aqmon.util.TimeUtils;
import edu.drexel.sd0910.ece01.aqmon.util.run.DelegatingExceptionProofRunnable;

/**
 * This class monitors the system's data to alert the user by e-mail if
 * there is data loss. Gmail is used to send the email notification.
 * 
 * @author Kyle O'Connor
 * 
 */
public class EmailNotificationManager extends Manager {

	private final static Logger log = Logger.getLogger(EmailNotificationManager.class);

	private final static String GMAIL_DOMAIN = "@gmail.com";
	private final static String ALERT_SUBJECT = "Alert from Air Quality Sensor Network (AQMon)";
	private final static String HTML_NEWLINE = "<br>";
	private final static int NUM_HOURS = 3;

	private String username;
	private String password;
	private String alertsAddresses;
	private String websiteURL;
	private Session gmailSession = null;
	
	/**
	 * Default Constructor for the <code>EmailNotificationManager</code>.
	 * 
	 * @param model
	 *            the <code>MainModel</code> object
	 * @param props
	 *            the <code>Properties</code> object from <code>MainModel</code>
	 */
	public EmailNotificationManager(MainModel model, Properties props) {
		super(model, props);
		username = props.getProperty(ConfigKeys.GMAIL_USERNAME);
		password = props.getProperty(ConfigKeys.GMAIL_PASSWORD);
		alertsAddresses = props.getProperty(ConfigKeys.ADDRESS_TO_ALERT);
		websiteURL = "<a href=" + props.getProperty(ConfigKeys.WEBSITE_URL) + ">Deployment page</a>";
		
		// create a new single thread executor
		super.setExecutorType(NEW_SINGLE_THREAD_SCHEDULED_EXECUTOR);
	}

	@Override
	public void startAllTasks() {
		log.debug("starting EmailNotificationManager tasks");
		// start exception proof email task every 2 hours
		((ScheduledExecutorService) getExecutorService()).scheduleAtFixedRate(new DelegatingExceptionProofRunnable(
				new DataMonitorTask()), TimeUtils.TWO_HOURS_IN_MS, TimeUtils.TWO_HOURS_IN_MS, TimeUnit.MILLISECONDS);
	}

	@Override
	public void stopAllTasks() {
		log.debug("canceling EmailNotificationManager tasks");
		stopExecutorService();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see edu.drexel.sd0910.ece01.aqmon.manager.Manager#shutdown()
	 */
	@Override
	public void shutdown() throws Exception {
		stopExecutorService();
	}

	/**
	 * Returns (or if needed, creates) a {@link javax.mail.Session Session} for
	 * Gmail using the username and password.
	 * 
	 * @return a mail <code>{@link javax.mail.Session Session}</code> object
	 *         configured for Gmail
	 */
	private Session getSession() {
		if (gmailSession == null) {
			Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());

			Properties props = new Properties();
			props.setProperty("mail.transport.protocol", "smtp");
			props.setProperty("mail.host", "smtp.gmail.com");
			props.put("mail.smtp.auth", "true");
			props.put("mail.smtp.port", "465");
			props.put("mail.smtp.socketFactory.port", "465");
			props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
			props.put("mail.smtp.socketFactory.fallback", "false");
			props.setProperty("mail.smtp.quitwait", "false");

			gmailSession = Session.getDefaultInstance(props, new Authenticator() {
				protected PasswordAuthentication getPasswordAuthentication() {
					return new PasswordAuthentication(username, password);
				}
			});
		}
		return gmailSession;
	}

	/**
	 * Attempts to send an email using the Gmail <code>Session</code> created by
	 * the {@link #getSession()} method. If an error occurs this method simply
	 * returns. The subject and to and from addresses are already specified. The
	 * body is inserted in the middle of a template alert email.
	 * 
	 * @param body
	 *            the body of the email message to send
	 */
	public synchronized void sendEmail(String body) {
		log.debug("Sending alert notification email using gmail");
		try {
			MimeMessage message = new MimeMessage(getSession());
			message.setSender(new InternetAddress(username + GMAIL_DOMAIN));
			message.setSubject(ALERT_SUBJECT);
			// create message body outline
			StringBuffer str = new StringBuffer();
			str.append("<html>");
			str.append("This is an alert from the Air Quality Sensor Network that you have deployed.");
			str.append(HTML_NEWLINE);
			str.append(HTML_NEWLINE);
			str.append("The system would like to inform you of the following information: ");
			str.append(HTML_NEWLINE);
			str.append(HTML_NEWLINE);
			str.append(body);
			str.append(HTML_NEWLINE);
			str.append("Please see the " + websiteURL
					+ " for more information and contact your support with any questions.");
			str.append(HTML_NEWLINE);
			str.append(HTML_NEWLINE);
			str.append("This notification was generated automatically. Please do not reply to this message.");
			str.append("</html>");
			message.setContent(str.toString(), "text/html");
			// set up recipient addresses
			if (alertsAddresses.indexOf(',') > 0) {
				message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(alertsAddresses));
			} else {
				message.setRecipient(Message.RecipientType.TO, new InternetAddress(alertsAddresses));
			}
			// send the message
			Transport.send(message);
			log.debug("Alert notification email sent!");
		} catch (Exception e) {
			// catch any and all exceptions
			log.error(e.toString());
			log.error("Exception occurred, alert email was not sent.");
			return; // exit back to the caller
		}
	}
	
	/**
	 * A <code>Runnable</code> task that sends an email alert if data loss
	 * occurs.
	 * 
	 * @author Kyle O'Connor
	 */
	private class DataMonitorTask implements Runnable {

		private static final String INACTIVE_NODE = " has not responded since range testing.";
		private static final String INACTIVE_TIME = " - data from this node has not been received for over "
				+ NUM_HOURS + " hours.";

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Runnable#run()
		 */
		public void run() {
			log.debug("DataMonitorTask is running...");

			List<String> inactiveNodes = new LinkedList<String>();
			List<ActiveNode> list = model.getXbeeManager().getActiveNodesList();
			Map<String, Date> map = model.getXbeeManager().getLastResponsesMap();
			synchronized (list) {
				// iterate through the active nodes list
				Iterator<ActiveNode> i = list.iterator();
				while (i.hasNext()) {
					ActiveNode an = i.next();
					// check if the last responses map contains all the node ids
					// in the active node list
					if (!map.containsKey(an.getNodeID())) {
						// if not, add to the inactive node list
						inactiveNodes.add(an.getNodeID() + INACTIVE_NODE);
						log.debug(an.getNodeID() + "is not in the LastResponsesMap");
					}
				}
			}
			// now check the times of each node in the map
			Set<String> nodeIDs = map.keySet();
			synchronized (map) {
				// check each node's last received time and see if its over 3 hours ago
				for (String id : nodeIDs) {
					if (TimeUtils.isOverXHoursAgo(map.get(id), NUM_HOURS)) {
						inactiveNodes.add(id + INACTIVE_TIME);
						log.debug(id + " lastResponse is over " + NUM_HOURS + " hours ago");
					}
				}
			}
			if (inactiveNodes.size() > 0) {
				log.info("Alert: data loss has occurred/there are inactive nodes");
				StringBuffer body = new StringBuffer();
				for (String item : inactiveNodes) {
					body.append(item + HTML_NEWLINE);
				}
				model.getEmailNotificationManager().sendEmail(body.toString());
			}

			log.debug("DataMonitorTask finished");
		}
	}

}
